/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package com.wider.servlet.multipart;

import com.wider.foundation.DLogger;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.SequenceInputStream;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;
import javax.mail.MessagingException;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMultipart;
import javax.mail.internet.MimePartDataSource;
import javax.mail.internet.MimeUtility;
import javax.naming.LimitExceededException;
import javax.naming.SizeLimitExceededException;
import javax.servlet.http.HttpServletRequest;

/**
 * Utility class for handling incoming HTML form data in the MIME-Multipart format.
 * To use this class, the JavaMail API (mailapi.jar) MUST be installed in your JAVA CLASSPATH accordingly
 * 
 * @author William Choi <avextk@gmail.com>
 */
public class MultipartFormData {
    static final int KB = 1024;
    static final int MB = 1024 * KB;
    
    private int     filelimit;
    private int     postlimit;
    private String  mpheader;
    private DLogger logger;
    private boolean mapvalid;
    private Vector	bigfiles;
    private Map<String, Object>     parammap;
    private Map<String, Object>     filesmap;
    
    private MimeMultipart           multipart;
    private HttpServletRequest      httpreq;

     /**
     * Default constructor for creating object representing the piece of the Multipart/form-data from the HTTP POST.
     * The size limit for each uploaded file will be 2MB as default.
     * @param httprequest   valid reference of the incoming HTTP request
     * @throws IOException
     * @throws LimitExceededException 

     */
    public MultipartFormData(HttpServletRequest httprequest) throws IOException, LimitExceededException {
        this(httprequest, 30*MB, 30*MB);
    }

     /**
     * Ultimate constructor for creating object representing the piece of the Multipart/form-data from the HTTP POST.
     * The size limit for each file will be explicitly specified by the user
     * @param httprequest   valid reference of the incoming HTTP request
     * @param post_limit    limit in MB for the data in each HTTP POST
     * @param file_limit    limit in MB for each part in the multipart/form-data of the HTTP POST
     * @throws IOException
     * @throws LimitExceededException 
     */
    public MultipartFormData(HttpServletRequest httprequest, int post_limit, int file_limit) throws IOException, LimitExceededException {
        if (!isMultipartFormData(httprequest)) {
            throw new IOException("Posted content isn't in type of multipart/form-data");
        }

        postlimit   = post_limit;
        filelimit   = file_limit;
        httpreq     = httprequest;
        mpheader    = httpreq.getContentType();
        mapvalid    = false;
        parammap    = null; //<ctrl-name, content> | <"files", List of file-names>
        filesmap    = null; //<file-name, partbody>

        //Get my logger for future use
        logger      = DLogger.getInstance(this);
        
        //Check the post size
        if (httpreq.getContentLength() > postlimit){
        	logger.warn("The HTTP Post exceeds the limit.");
        	throw new LimitExceededException("HTTP 413; Request Entity Too Large");
        }
        
        //Get MIME Multipart structure from the POSTed content, and save it in private member 'multipart'
        multipart   = getMimeMultipart();
        
        //Generate the parameter map
        genParamMap();

    }
    
    public static boolean isMultipartFormData(HttpServletRequest httprequest) throws IOException{
        try{
            return httprequest.getContentType().toLowerCase().contains("multipart/form-data");
        }
        catch (Exception e){
            throw new IOException("Invalid Request reference or posted was corrupted.");
        }
    }

    public Map<String, Object> getParameterMap() throws IOException{
        if (!mapvalid)
            return null;

        return parammap;
    }


    public Object getParameter(String key) throws IOException{
        if (!mapvalid)
            return null;

        return parammap.get(key);
    }

    public Enumeration<String> getParameterNames() throws IOException{
        if (!mapvalid)
            return null;

        return Collections.enumeration(parammap.keySet());
    }

    /*
    public Object[] getParameterValues(String name) throws IOException{
        if (!mapvalid)
            genParamMap();
        
        //?? this way returns the entire values set, i.e. not related to the String name
        return parammap.values().toArray();
    }
    */
    
    public int saveFile(String filename, String destination) throws IOException{
        try {
            int ret = 0;
            MimeBodyPart bodypart = (MimeBodyPart) filesmap.get(filename);

            File file = new File(destination, filename);
            bodypart.saveFile(file);
            return ++ret;
        } catch (MessagingException ex) {
            logger.error(ex);
            throw new IOException("JavaMail MessagingException occurs while saving file.");
        }
    }

    public int saveFiles(Vector filenames, String destination) throws IOException{
        int ret = 0;

        for (String filename : (Vector<String>) filenames){
            try {
                MimeBodyPart bodypart = (MimeBodyPart) filesmap.get(filename);
                
                File file = new File(destination, filename);
                bodypart.saveFile(file);
                ret++;
            } catch (MessagingException ex) {
                logger.error(ex);
            }
        }

        return ret;
    }

    public void checkCompleteness() throws SizeLimitExceededException{
    	if (!bigfiles.isEmpty()){
    		throw new SizeLimitExceededException("HTTP 206; Partial Content; files: " + bigfiles.toString() + " exceed the limit " + filelimit + ".");
    	}
    }
    
    private MimeMultipart getMimeMultipart() throws IOException{

        mpheader = mpheader + "\r\n\r\n";
        SequenceInputStream mimestream = new SequenceInputStream(new ByteArrayInputStream(mpheader.getBytes()), httpreq.getInputStream());
        
        try {
            MimePartDataSource mimesrc = new MimePartDataSource(new MimeBodyPart(mimestream));
            MimeMultipart multi = new MimeMultipart(mimesrc);

            return multi;
         } catch (Exception ex) {
            logger.error(ex);
            throw new IOException("Failed to construct the MIME Multipart object.");
        }
    }

    private String getPartControlName(MimeBodyPart bodypart) throws IOException{
        Enumeration headers;
        String  name        = null;
        String  stack       = null;
        String  needle      = "name=";
        char[]  delimiters  = {';', ' ', '\r', '\n'};
        
        try {
            headers = bodypart.getAllHeaderLines();
        } catch (Exception ex) {
            logger.error(ex);
            throw new IOException("Exception occurs while operating on the MimeMultipart object");
        }
        
        while(headers.hasMoreElements()){
            int     index_start =  0;
            int     index_stop  = -1;

            stack = headers.nextElement().toString().toLowerCase();
            index_start = stack.indexOf(needle);
            if (index_start < 0)
                continue;
            index_start += needle.length();

            int j = 0;
            while ((index_stop < 0) && (j < delimiters.length)){
                index_stop = stack.indexOf(delimiters[j], index_start);
                j++;
            }
            if (index_stop < 0)
                index_stop = stack.length();

            name = stack.substring(index_start, index_stop);
            break; //no need to iterate to the next header element;
        }
        return name;
    }

    private void genParamMap() throws IOException {
        MimeBodyPart    bodypart    = null;
        String          ctrlname    = null;
        String          disposition = null;
        
        try {
            //Allocate the memory for the Maps
            bigfiles = new Vector<String>();
            parammap = new HashMap<String, Object>();
            filesmap = new HashMap<String, Object>();
        	
            //Iterate each bodypart within the multipart to fill up the Map of param and files
            for (int i = 0; i < multipart.getCount(); i++) {
                bodypart    = (MimeBodyPart) multipart.getBodyPart(i);
                ctrlname    = getPartControlName(bodypart);
                disposition = bodypart.getDisposition();

                if (!disposition.equalsIgnoreCase("file")){
                    parammap.put(ctrlname, MimeUtility.decodeText(bodypart.getContent().toString()));
                }else{
                    String filename = MimeUtility.decodeText(bodypart.getFileName());
                    if (null == filename)
                    	continue;
                    
                    //Check if this part exceeds the limit
                    if (bodypart.getSize() > filelimit){
                    	bigfiles.add(filename);
                    	continue;
                    }
                    
                    Vector<String> filenames = null;
                    if (null == (filenames = (Vector<String>) parammap.get(ctrlname))){
                        filenames = new Vector<String>();
                    }
                    filenames.add(filename);
                    parammap.put(ctrlname, filenames);
                    filesmap.put(filename, bodypart);
                }
            }

            //Set the valid flag so that this method will no longer be called.
            mapvalid = true;
            
        } catch (Exception ex) {
            throw new IOException("Exception occurs while operating on the MimeMultipart object");
        }
    }
}
